Skip to content
This repository has been archived by the owner on Aug 23, 2023. It is now read-only.

replace statsd[aemon] metrics with built-in metrics library #384

Closed
wants to merge 36 commits into from

Conversation

Dieterbe
Copy link
Contributor

@Dieterbe Dieterbe commented Nov 13, 2016

just adding my prototype new library.
didn't do any work integrating it into metrictank yet because the code base needs to stabilize first (e.g. after merging at least the http refactor, potentially also clustering)

buf = WriteUint32(buf, l.prefix, []byte("p75"), r.P75/1000, now)
buf = WriteUint32(buf, l.prefix, []byte("p90"), r.P90/1000, now)
buf = WriteUint32(buf, l.prefix, []byte("max"), r.Max/1000, now)
buf = WriteUint32(buf, l.prefix, []byte("count"), r.Count/1000, now)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think you should be dividing count by 1000

@Dieterbe
Copy link
Contributor Author

Dieterbe commented Nov 27, 2016

now rebased on top of clustering. made some good progress, but still a bunch of things to switch over.
also added another artisanalhistogram type for "slower" things (500ms ~ 12h) and a plain histogram type for non-timing things.

@Dieterbe Dieterbe self-assigned this Dec 15, 2016
@Dieterbe Dieterbe added this to the hosted-metrics-alpha milestone Dec 15, 2016
@Dieterbe Dieterbe force-pushed the builtin-metrics branch 7 times, most recently from 6428ed6 to a7aabea Compare December 22, 2016 09:21
@Dieterbe
Copy link
Contributor Author

Dieterbe commented Dec 22, 2016

Status:

  • todo: more testing especially memory usage and under a http workload (which means i will work on the new index dump tool cause i need that to feed vegeta)
  • todo: add provision to clear the stats registry in between unit tests (see build failures)
  • todo: update metric descriptions, run metrics2docs, update ops guide
  • that said, ready for reviewing again

Some interesting notes (please also read the commit messages for more details):

  • under a workload of switching from 80k ingest to 180k (e.g. lots of index writes), as well as under a 180k steady workload using the carbon input, stats.(*Meter32).ValueUint32 shows up taking 0.5% cpu, that's the only stats function in the profile
  • I decided to put the metric type at the end of each metric. the types are named after http://metrics20.org/spec/#tag-values-mtype but also include the bit size. (note gauge1 for booleans. this may be a bit too cryptic, but bool is not a metrics2.0 mtype). Contrast to statsd where it puts the type very early on in the tree. I like this more because it allows putting related measurements together better, makes it easier to navigate the tree (because with statsd you often don't know what type it is and need to search through multiple subtrees) and it also makes things a bit more clear what a metric means exactly (like counter has to be derived to get the rate). see 25f9c34 for more thoughts on this.
  • right now a bunch of the values are reported as counters, I like this. but it does mean it needs to be derived (using perSecond() ), which is a slight extra hassle. we can consider reporting it as a rate.
  • several of the metrics used to be in arbitrary places, now everything is more neatly organized in sections. (api, cassandra, idx, input, ...) see https://gist.github.com/Dieterbe/2855e2a8d534ac7021fcb9d1d269b937 for a current dump. (note tank which is metrics about the in memory storage). some more changes I'ld like to make:
    • notifier -> cluster.notifier
    • cassandra -> store.cassandra (contrast to idx.cassandra)
    • longer term we can also refine this more, and add per http-path latencies under api in a more structured way.
  • the included dashboard.json has been updated and i verified all metrics in all panels, everything should be good. though later it may make more sense to remodel the dashboard a bit, and do rows per "thing" (e.g. a row for global stats, a row for tank metrics, a row per input, etc). We have this already more or less, but it could be clearer and more explicit
  • while we use basic histograms for things like chunk sizes, when we measure latencies that we know should be within specific ranges (e.g. 1ms ~15s) I'm trying out a "human friendly" histogram class (see https://github.com/Dieterbe/artisanalhistogram). once grafana can display heatmaps, this will provide class boundaries on human friendly boundaries (even numbers). but still need to do the http tests to see if it works

if you want to run it and see for yourself,it's easy:

make bin
cd docker/docker-dev-custom-cfg-kafka
docker-compose down
docker-compose up --force-recreate
(.. wait until everything is up)
../extra/populate-grafana.sh dev-custom-cfg-kafka  # this installs the datasource and dashboard
then go to localhost:3000 in your browser and load metrictank dashboard, change datasource to metrictank

log.Fatal("invalid output")
}

// from should either a unix timestamp, or a specification that graphite/metrictank will recognize.
Copy link
Contributor

@replay replay Dec 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either be a

@Dieterbe Dieterbe force-pushed the builtin-metrics branch 4 times, most recently from 46daacc to f6b9b31 Compare December 27, 2016 20:13
@Dieterbe
Copy link
Contributor Author

running ./build/mt-index-cat -from=60min cass -keyspace raintank vegeta-render | vegeta attack -rate 1000 -duration 2m under a 50kHz ingest, and metrictank.stats is about 0.5% of cpu.

as for memory used, all memory of the stats library (under ingest +http workload) is due to the graphite queue, which can be sized as desired.

@Dieterbe Dieterbe force-pushed the builtin-metrics branch 2 times, most recently from 8f86076 to 975c33b Compare December 27, 2016 22:56
@Dieterbe
Copy link
Contributor Author

@woodsaj @replay I just have to fix the end2end test, otherwise I think this in good shape for merging. time for reviewing :)

@Dieterbe Dieterbe changed the title WIP replace statsd[aemon] metrics with built-in metrics library replace statsd[aemon] metrics with built-in metrics library Dec 28, 2016
we just update it every time we add or remove metrics.
technically we only need the value once every second,
but the previous approach would do it at a time not matching
when we send metrics.  Now we could synchronize them via a callback
but it would be a little too messy.

we can do this about 200M/second so we're good.

package mdata

import (
	"sync/atomic"
	"testing"
)

var V uint32

func Benchmark_Uint32(b *testing.B) {
	var v uint32

	for i := 0; i < b.N; i++ {
		atomic.StoreUint32(&v, uint32(i))
	}
	V = v
}

go test -run='^$' -bench=Uint
Benchmark_Uint32-8   	300000000	         5.14 ns/op
PASS
ok  	github.com/raintank/metrictank/mdata	2.068s
the filled areas was a bit too much. the lines are tighter.
also null=connected because this overflows over ~4billion very regularly
most times you import the dash (docker stack, fresh install) you want to zoom in
on recent stats.  Seems more common than importing a dash for an already
existing setup.
@Dieterbe
Copy link
Contributor Author

@replay what do you think of the new graphite conn routine? i haven't subjected it to the tests outlined in #384 (comment), but will, if the code looks good to you.
also what do you think of 767502e ?

c.msgsConnections.Set(s.Connections)
}
}()
return &c, err
Copy link
Contributor

@replay replay Dec 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there was an err at nsq.NewConsumer() wouldn't it probably be better to return right there, without creating a go routine that's probably going to fail once a second

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah

func NewErrMetrics(component string) ErrMetrics {
return ErrMetrics{

// metric idx.cassandra.error.timeout is a counter of timeouts seen to the cassandra idx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems outdated, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cleared this up on slack. but basically https://github.com/Dieterbe/metrics2docs is a bit simplistic, it can't parse the code very well, and as there are 2 places where we dynamically construct these metrics:

mdata/store_cassandra.go
68:    errmetrics = cassandra.NewErrMetrics("store.cassandra")

idx/cassandra/cassandra.go
48:    errmetrics           = cassandra.NewErrMetrics("idx.cassandra")

we need to add two comments for each metric here, and there needs to be an empty line between them because otherwise metrics2docs fails

## misc ##

# instance identifier. must be unique. used in clustering messages, for naming queue consumers and emitted metrics.
instance = defaulti
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, but is that a typo?

# The default matches what the Grafana dashboard expects
# $instance will be replaced with the `instance` setting.
# note, the 3rd word describes the environment you deployed in.
prefix = metrictank.stats.defaulte.$instance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is defaulte another typo or do i just totally not get what you did there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was because i wanted to make sure the environment and instance showed up in the right place (default.default was a bit ambiguous). but I can undo that now and set both to default

}

func (g *Gauge64) Dec() {
atomic.AddUint64(&g.val, ^uint64(0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

m.max = val
}
c := m.hist[bin] + 1
m.hist[bin] = c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't this be m.hist[bin]++? c does not seem needed

for _, k := range keys {
key := uint32(k)
runningcount += m.hist[key]
runningsum += uint64(m.hist[key]) * uint64(key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does that get multiplied with uint64(key)? maybe a comment would be good

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the running sum. so it has to multiply the value in the bucket with the number of samples that had that value.

for {
select {
case <-connectTicker:
conn = assureConn()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry that i keep going on about this :) but what's the purpose of still having this connectTicker here?

  • when a new message gets read from g.toGraphite ok will be initialized to false on line 115, so the conn will be created by assureConn() on 117
  • if during writing the connection fails then err will be set to != nil on line 119, so ok never gets set to true, so it will loop on the for at 116 and reconnect at 117

Why still have the ticker then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha you're right again I think.
let me do another round of simplifying :)

func (r *Registry) add(name string, metric GraphiteMetric) GraphiteMetric {
r.Lock()
if _, ok := r.metrics[name]; ok {
panic(fmt.Sprintf(errFmtMetricExists, name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really dont think this is an un-recoverable error. We should return an error instead.

Perhaps it would be better to use an addOrGet() method that either returns an existing graphiteMetric with the specified name or creates the new one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think AddOrGet is a good idea.
However, the pre-existing metric may be a different type. E.g. trying to register a Bool, but there's already a Meter32 or something. There's not much useful we can do in this scenario (we could keep running but without the instrumentation, which I think is pretty dangerous and we'd have to make the code more complicated to support a graphite metric type which is a no-op). So if the pre-existing one is different type, i think a panic is still warranted.

if pre-existing is the same type, then we can totally just return that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good.

@Dieterbe
Copy link
Contributor Author

Dieterbe commented Jan 3, 2017

made the change woodsaj requested, tested the new loop using the same experiments as #384 (comment) and it all still works as before.

merged to master from commandline.

@Dieterbe Dieterbe closed this Jan 3, 2017
@Dieterbe Dieterbe deleted the builtin-metrics branch September 18, 2018 09:03
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants